﻿/// <reference path="custommanipulation.d.ts" />
/// <reference path="../../../scripts/typings/winjs/winjs.d.ts" />
import $ = require("jquery");
import ko = require("knockout");
import CallLimiter = require("../../utils/CallLimiter");

function clamp(num, min, max) {
    if (num < min) return min;
    if (max < num) return max;
    return num;
}

class CustomScroll implements ICustomScroll {
    private $target: JQuery;
    private $grid: JQuery;
    private $horizontal: JQuery;
    private $vertical: JQuery;

    private grid: HTMLElement;
    private target: ManipElement;
    private horizontal: HTMLElement;
    private vertical: HTMLElement;

    public constructor($target: JQuery, public changeScrollCallback: (args:IScrollChangedArgs)=>boolean|void) {
        this.$target = $target;
        this.target = <ManipElement>($target[0]);
        this.target.ManipScrollInstance = this;
        this.createScrollbars();
        this.bindEvents();
        this.reset();
        this.translate();

        this.scrollLimiter = CallLimiter((args: any) => {
            this.scrollRelative(args.relx,args.rely);
        }, (oldArgs, newArgs) => {
            newArgs.relx += oldArgs.relx;
            newArgs.rely += oldArgs.rely;
            return newArgs;
        });
    }

    createScrollbars() {
        const dummy = document.createElement("div");
        WinJS.Utilities.setInnerHTML(dummy,
            "<div class='custom-manip-scrollbar-grid'>" +
            "   <div class='custom-manip-scrollbar horizontal'><div class='custom-manip-scroll-dummy-content'></div></div>" +
            "   <div class='custom-manip-scrollbar vertical'><div class='custom-manip-scroll-dummy-content'></div></div>" +
            "   <div class='custom-mainp-scroll-content'></div>" +
            "</div>");

        const grid = <HTMLElement>dummy.firstChild;
        const $grid = $(dummy.firstChild);

        this.$grid = $grid;
        this.grid = grid;
        this.$horizontal = $grid.find(".horizontal");
        this.horizontal = this.$horizontal[0];
        this.$vertical = $grid.find(".vertical");
        this.vertical = this.$vertical[0];
        this.$content = $grid.find(".custom-mainp-scroll-content");
        this.content = this.$content[0];

        while (this.target.childNodes.length > 0) {
            this.content.appendChild(this.target.firstChild);
        }

        this.target.appendChild(this.grid);
    }

    reset() {
        this.$content.css("overflow", "visible");
        this.target.ManipScrollX = 0;
        this.target.ManipScrollY = 0;
        this.originalTransform = this.target.style.transform;
    }

    bindEvents() {
        this.$grid.on("manip.scroll", (event, args: IScrollChangedArgs) => {
            //this.limitedScroll(args);
            this.scrollLimiter(args);
            event.stopPropagation();
        });

        this.$vertical.on("manip.scroll", (event) => {
            event.stopPropagation();
        });

        this.$horizontal.on("manip.scroll", (event) => {
            event.stopPropagation();
        });

        var prevTop = this.vertical.scrollTop;
        this.vertical.addEventListener("scroll", (event) => {
            if (this.ignoreTop) {
                this.ignoreTop = false;
                return;
            }
            var diff = (this.vertical.scrollTop - prevTop) / this.vertical.scrollHeight;
            this.scrollRelative(0, diff * this.content.scrollWidth);
            prevTop = this.vertical.scrollTop;
        }, false);

        var prevLeft = this.vertical.scrollLeft;
        this.horizontal.addEventListener("scroll", (event) => {
            if (this.ignoreLeft) {
                this.ignoreLeft = false;
                return;
            }
            var diff = (this.horizontal.scrollLeft - prevLeft) / this.horizontal.scrollWidth;
            this.scrollRelative(diff * this.content.scrollHeight, 0);
            prevLeft = this.horizontal.scrollLeft;
        }, false);

        /**
         * Show scrollbars on hover
         */
        this.grid.addEventListener("mousemove", () => {
            var prevx = this.horizontal.scrollLeft;
            var prevy = this.vertical.scrollTop;
            this.ignoreTop = true;
            this.ignoreLeft = true;
            this.horizontal.scrollLeft += 100;
            this.vertical.scrollTop += 100;
            this.horizontal.scrollLeft = prevx;
            this.vertical.scrollTop = prevy;
        }, false);

        this.grid.addEventListener("wheel", (event) => {
            if (event.ctrlKey) {
                return;
            }
            var dict = {};
            dict[event.DOM_DELTA_LINE] = 20;
            dict[event.DOM_DELTA_PAGE] = 600;
            dict[event.DOM_DELTA_PIXEL] = 1;
            var deltaX = dict[event.deltaMode] * event.deltaX;
            var deltaY = dict[event.deltaMode] * event.deltaY;

            if (getComputedStyle(this.target).getPropertyValue("-ms-scroll-translation") == "vertical-to-horizontal" && !event.altKey && !event.shiftKey) {
                deltaX += deltaY;
                deltaY = 0;
            }

            this.scrollRelative(deltaX, deltaY);

            event.stopPropagation();
        });

        var keySpeed = 20;
        this.grid.addEventListener("keydown", (event) => {
            switch (event.keyCode) {
                case 37:
                    this.scrollRelative(-keySpeed, 0);
                    break;
                case 39:
                    this.scrollRelative(keySpeed, 0);
                    break;
                case 38:
                    this.scrollRelative(0, -keySpeed);
                    break;
                case 40:
                    this.scrollRelative(0, keySpeed);
                    break;
                default:
                    return;
            }
            keySpeed = clamp(keySpeed *1.1, 0, 120);
        });
        this.grid.addEventListener("keyup", (event) => {
                if (event.keyCode >= 37 && event.keyCode <= 40) {
                    keySpeed = 20;
                }
        });
    }

    translate() {
        var sx = this.target.ManipScrollX;
        var sy = this.target.ManipScrollY;
        var sw = this.content.scrollWidth;
        var sh = this.content.scrollHeight;
        var w = this.target.offsetWidth;
        var h = this.target.offsetHeight;

        var matrix = new MSCSSMatrix();
        matrix = matrix.translate(-sx, -sy);
        (<any>(this.content.style)).transform = <any>matrix;

        var horScrollBarRatio = this.horizontal.offsetWidth / w;
        var verScrollBarRatio = this.vertical.offsetHeight / h;

        (<HTMLElement>(this.horizontal.firstChild)).style.width =
            (sw * horScrollBarRatio) + "px";
        (<HTMLElement>(this.vertical.firstChild)).style.height =
            (sh * verScrollBarRatio) + "px";

        var scrollLeft = (sx * horScrollBarRatio);
        var scrollTop = (sy * verScrollBarRatio);

        this.horizontal.scrollLeft = scrollLeft;
        this.vertical.scrollTop = scrollTop;
        this.ignoreLeft = true;
        this.ignoreTop = true;
    }

    scrollRelative(relx: number, rely: number) {
        if (relx == 0 && rely == 0) return;
        this.target.ManipScrollX = clamp(this.target.ManipScrollX + relx, 0, this.target.scrollWidth - this.target.clientWidth);
        this.target.ManipScrollY = clamp(this.target.ManipScrollY + rely, 0, this.target.scrollHeight - this.target.clientHeight);
        this.translate();
        this.changeScrollCallback({
            x: this.target.ManipScrollX,
            y: this.target.ManipScrollY,
            relx: relx,
            rely: rely
        });
    }

    //private limitedScroll = (args: IScrollChangedArgs) => {
    //    if (this.nextScrollTimeout === null) {
    //        this.scrollRelative(args.relx, args.rely);
    //        this.nextScrollTimeout = requestAnimationFrame(() => {
    //            this.nextScrollTimeout = null;
    //            if (this.nextRelScroll) {
    //                this.scrollRelative(this.nextRelScroll.relx, this.nextRelScroll.rely);
    //                this.nextRelScroll = null;
    //            }
    //        });
    //    } else {
    //        if (this.nextRelScroll === null) {
    //            this.nextRelScroll = args;
    //        } else {
    //            var next = this.nextRelScroll;
    //            args.relx += next.relx;
    //            args.rely += next.rely;
    //            this.nextRelScroll = args;
    //        }
    //    }
    //}

    originalTransform: string;
    ignoreLeft: boolean = false;
    ignoreTop: boolean = false;
    private $content: JQuery;
    private content: HTMLElement;
    //private nextScrollTimeout: number = null;
    //private nextRelScroll: IScrollChangedArgs = null;
    private scrollLimiter: LimitedCallback<IScrollChangedArgs>;
};

class Scroll {
    public static create() {
        ko.bindingHandlers.CustomScroll = {
            init: (element, valueAccessor: () => KnockoutObservable<IScrollChangedArgs>|any) => {
                var observable = valueAccessor();

                if (ko.isObservable(observable)) {
                    var customScroll = new CustomScroll($(element), (value: IScrollChangedArgs) => {
                        valueAccessor()(value);
                    });

                    valueAccessor().subscribe((value) => {
                        customScroll.scrollRelative(
                            value.x - element.ManipScrollX,
                            value.y - element.ManipScrollY
                        );
                    });
                } else {
                    // ReSharper disable once WrongExpressionStatement
                    new CustomScroll($(element),(value:any)=>{});
                }
            },
            update: () => { }
        }
    }
}

export=Scroll;